/**
 * Copyright (c) 2023 murenchao
 * taomu is licensed under Mulan PubL v2.
 * You can use this software according to the terms and conditions of the Mulan PubL v2.
 * You may obtain a copy of Mulan PubL v2 at:
 *       http://license.coscl.org.cn/MulanPubL-2.0
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PubL v2 for more details.
 */
package cool.taomu.box.asm

import cool.taomu.box.asm.entity.ClassEntity
import cool.taomu.box.asm.entity.FieldEntity
import cool.taomu.box.asm.entity.MethodEntity
import java.util.concurrent.ConcurrentHashMap
import org.eclipse.xtend.lib.annotations.Accessors
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.commons.GeneratorAdapter
import org.objectweb.asm.commons.Method

@Accessors
class CreateMethod {
	GeneratorAdapter ga;
	ClassEntity classEntity;
	var String internalName;
	val localVariable = new ConcurrentHashMap<String, Variable>();

	@Accessors
	static final class Variable {
		Integer index;
		Type type;

		new(Integer index, Type type) {
			this.index = index;
			this.type = type;
		}
	}

	static interface Instruct {
		def void coding(CreateMethod cm);
	}

	new(ClassEntity classEntity, GeneratorAdapter ga) {
		this.ga = ga;
		this.classEntity = classEntity;
		this.internalName = #[classEntity.packageName, classEntity.className].join(".");
	}

	/**
	 * TODO 以后尝试修改
	 */
	def innerClass(String name, int num) {
		this.classEntity.cwriter.visitInnerClass(#[name, num].join("$"), null, null, Opcodes.ACC_PRIVATE);
	}

	def invokeVirtual(MethodEntity method) {
		if (method.method !== null) {
			ga.invokeInterface(Type.getObjectType(method.internalName.replace(".", "/")),
				Method.getMethod(method.method));
		} else {
			var String internalName = this.internalName.replace(".", "/")
			if (!method.internalName.nullOrEmpty) {
				internalName = method.internalName;
			}
			ga.invokeVirtual(Type.getObjectType(internalName),
				Method.getMethod(method.defineMethod, method.defaultPackage));

		}
		return this;
	}

	def invokeInterface(MethodEntity method) {
		if (method.defineMethod.nullOrEmpty) {
			ga.invokeInterface(Type.getObjectType(method.internalName.replace(".", "/")),
				Method.getMethod(method.method));
		} else {
			ga.invokeInterface(Type.getObjectType(method.internalName.replace(".", "/")),
				Method.getMethod(method.defineMethod, method.defaultPackage));

		}
		return this;
	}

	def invokeStatic(MethodEntity method) {
		if (method.defineMethod.nullOrEmpty) {
			ga.invokeStatic(Type.getObjectType(method.internalName.replace(".", "/")), Method.getMethod(method.method));
		} else {
			ga.invokeStatic(Type.getObjectType(method.internalName.replace(".", "/")),
				Method.getMethod(method.defineMethod, method.defaultPackage));

		}
		return this;
	}

	def invokeSpecial(MethodEntity method) {
		if (method.defineMethod.nullOrEmpty) {
			ga.invokeConstructor(Type.getObjectType(method.internalName.replace(".", "/")),
				Method.getMethod(method.method));
		} else {
			ga.invokeConstructor(Type.getObjectType(method.internalName.replace(".", "/")),
				Method.getMethod(method.defineMethod, method.defaultPackage));

		}
		return this;
	}

	def getStatic(String internalName, String name, String type) {
		ga.getStatic(Type.getObjectType(internalName.replace(".", "/")), name, Type.getType(type))
		return this;
	}

	def getField(FieldEntity field) {
		ga.loadThis;
		ga.getField(Type.getObjectType(this.internalName.replace(".", "/")), field.name,
			Type.getType(field.descriptor));
		return this;
	}

	def setField(FieldEntity field, Instruct is) {
		ga.loadThis;
		if (is !== null) {
			is.coding(this);
		}
		ga.putField(Type.getObjectType(this.internalName.replace(".", "/")), field.name, Type.getType(field.descriptor))
		return this;
	}

	def store(String name, Class<?> zlass) {
		var type = Type.getType(zlass);
		var index = ga.newLocal(type);
		ga.storeLocal(index, type);
		this.localVariable.put(name, new Variable(index, Type.getType(zlass)));
		return this;
	}

	def load(String name) {
		if (this.localVariable.containsKey(name)) {
			val local = this.localVariable.get(name);
			ga.loadLocal(local.index, local.type);
		} else {
			// throw new UnknownEntityException(name);
		}
		return this;
	}

	def This() {
		ga.loadThis;
		return this;
	}

	def pop() {
		ga.pop;
		return this;
	}

	def dispatch ldc(Type type) {
		ga.push(type);
		return this;
	}

	def dispatch ldc(String value) {
		ga.push(value);
		return this;
	}

	def dispatch ldc(int value) {
		ga.push(value);
		return this;
	}

	def dispatch ldc(long value) {
		ga.push(value);
		return this;
	}

	def dispatch ldc(short value) {
		ga.push(value);
		return this;
	}

	def dispatch ldc(double value) {
		ga.push(value);
		return this;
	}

	def dispatch ldc(float value) {
		ga.push(value);
		return this;
	}

	def dispatch ldc(boolean value) {
		ga.push(value);
		return this;
	}

	def dispatch ldc(char value) {
		ga.push(value);
		return this;
	}

	def NEW(Class<?> zlass) {
		ga.newInstance(Type.getType(zlass));
		return this;
	}

	def dup() {
		ga.dup;
		return this;
	}

	def returnValue() {
		ga.returnValue();
		return this;
	}

	def endMethod() {
		ga.endMethod();
		return this;
	}

	// TODO 
	def checkCast(Class<?> type) {
		ga.checkCast(Type.getType(type));
		return this;
	}

	def invokeVirtual(Class<?> zlass, String method) {
		return this.invokeVirtual(zlass.name, method);
	}

	def invokeVirtual(String internalName, String method) {
		return this.invokeVirtual(internalName, method, true);
	}

	def invokeVirtual(String internalName, String method, boolean defaultPackage) {
		ga.invokeVirtual(Type.getObjectType(internalName.replace(".", "/")), Method.getMethod(method, defaultPackage));
		return this;
	}

	def buildReturnValue(java.lang.reflect.Method m) {
		switch (m.returnType) {
			case int: {
				this.checkCast(Integer);
				this.invokeVirtual(Integer, "int intValue()");
			}
			case long: {
				this.checkCast(Long);
				this.invokeVirtual(Long, "long longValue()");
			}
			case double: {
				this.checkCast(Double);
				this.invokeVirtual(Double, "double doubleValue()");
			}
			case float: {
				this.checkCast(Float);
				this.invokeVirtual(Float, "float floatValue()");
			}
			case boolean: {
				this.checkCast(Boolean);
				this.invokeVirtual(Boolean, "boolean booleanValue()");
			}
			case short: {
				this.checkCast(Short);
				this.invokeVirtual(Short, "short shortValue()");
			}
			case char: {
				this.checkCast(Character);
				this.invokeVirtual(Character, "char charValue()");
			}
			case byte: {
				this.checkCast(Byte);
				this.invokeVirtual(Byte, "byte byteValue()");
			}
			case void: {
			}
			default: {
				this.checkCast(m.returnType);
			}
		}
		return this;
	}

	def buildArguments(Method method) {
		if (method.argumentTypes.size > 0) {
			for (var index = 0; index < method.argumentTypes.size; index++) {
				ga.dup();
				ga.push(index);
				switch (method.argumentTypes.get(index).descriptor) {
					case "Z": {
						ga.loadArg(index);
						this.invokeStatic(Boolean, "Boolean valueOf(boolean)");
					}
					case "C": {
						ga.loadArg(index);
						this.invokeStatic(Character, "Character valueOf(char)");
					}
					case "B": {
						ga.loadArg(index);
						this.invokeStatic(Byte, "Byte valueOf(byte)");
					}
					case "S": {
						ga.loadArg(index);
						this.invokeStatic(Short, "Short valueOf(short)");
					}
					case "I": {
						ga.loadArg(index);
						this.invokeStatic(Integer, "Integer valueOf(int)");
					}
					case "F": {
						ga.loadArg(index);
						this.invokeStatic(Float, "Float valueOf(float)");
					}
					case "J": {
						ga.loadArg(index);
						this.invokeStatic(Float, "Long valueOf(long)");
					}
					case "D": {
						ga.loadArg(index);
						this.invokeStatic(Float, "Double valueOf(double)");
					}
					default: {
						ga.visitVarInsn(Opcodes.ALOAD, index + 1);
					}
				}
				ga.visitInsn(Opcodes.AASTORE);
			}
		} else {
			ga.visitInsn(Opcodes.ACONST_NULL);
		}
		return this;
	}

	def invokeStatic(Class<?> type, String method) {
		return this.invokeStatic(type.name, method, true);
	}

	def invokeStatic(String internalName, String method, boolean defaultPackage) {
		ga.invokeStatic(Type.getObjectType(internalName.replace(".", "/")), Method.getMethod(method, defaultPackage));
		return this;
	}

}
